//========================================================================
//Copyright 2007-2009 David Yu dyuproject@gmail.com
//------------------------------------------------------------------------
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at 
//http://www.apache.org/licenses/LICENSE-2.0
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.
//========================================================================

parser grammar ProtoParser;

options {

    // Default language but name it anyway
    //
    language  = Java;

    // Produce an AST
    //
    output    = AST;

    // Use a superclass to implement all helper
    // methods, instance variables and overrides
    // of ANTLR default methods, such as error
    // handling.
    //
    superClass = AbstractParser;

    // Use the vocabulary generated by the accompanying
    // lexer. Maven knows how to work out the relationship
    // between the lexer and parser and will build the 
    // lexer before the parser. It will also rebuild the
    // parser if the lexer changes.
    //
    tokenVocab = ProtoLexer;
}

// Some imaginary tokens for tree rewrites
//

// What package should the generated source exist in?
//
@header {
    package io.protostuff.parser;
}

parse [Proto proto]
    :   (statement[proto])+ EOF! {
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
            
            proto.postParse();
        }
    ;
    
statement [Proto proto]
    :   header_syntax[proto]
    |   header_package[proto]
    |   header_import[proto]
    |   message_block[proto, null]
    |   enum_block[proto, null]
    |   extend_block[proto, null]
    |   service_block[proto, null]
    |   annotation_entry[proto]
    |   doc_entry[proto]
    |   option_entry[proto, proto]
    ;

// some keywords that might possibly be used as a variable
var_reserved
    :   TO | PKG | SYNTAX | IMPORT | OPTION | MESSAGE | SERVICE | ENUM |  
        REQUIRED | OPTIONAL | REPEATED | EXTENSIONS | EXTEND | GROUP | RPC | 
        RETURNS | INT32 | INT64 | UINT32 | UINT64 | SINT32 | SINT64 | 
        FIXED32 | FIXED64 | SFIXED32 | SFIXED64 | FLOAT | DOUBLE | BOOL | 
        STRING | BYTES | DEFAULT | MAX | VOID
    ; 

var
    :   ID | var_reserved
    ;

var_full
    :   FULL_ID | var
    ;

annotation_entry [Proto proto]
@init {
    Annotation annotation = null;
}
    :   AT var { annotation = new Annotation($var.text); }
        (LEFTPAREN 
        annotation_keyval[proto, annotation] (COMMA annotation_keyval[proto, annotation])* 
        RIGHTPAREN)? {
            proto.add(annotation);
        }
    ;

annotation_keyval [Proto proto, Annotation annotation]
    :   k=var_full ASSIGN (
                vr=var_reserved { annotation.put($k.text, $vr.text); }
            |   ID { annotation.putRef($k.text, $ID.text); }
            |   fid=FULL_ID { annotation.putRef($k.text, $fid.text); }
            |   NUMFLOAT { annotation.put($k.text, Float.valueOf($NUMFLOAT.text)); }
            |   NUMINT { annotation.put($k.text, Integer.valueOf($NUMINT.text)); }
            |   NUMDOUBLE { annotation.put($k.text, Double.valueOf($NUMDOUBLE.text)); }
            |   TRUE { annotation.put($k.text, Boolean.TRUE); }
            |   FALSE { annotation.put($k.text, Boolean.FALSE); }
            |   STRING_LITERAL { annotation.put($k.text, getStringFromStringLiteral($STRING_LITERAL.text)); }
        )
    ;

doc_entry [Proto proto]
    :   DOC {
            String comment = $DOC.text;
            // remove leading triple slash and trailing spaces/newline
            int len = comment.length();
            while ((0 < len) && (comment.charAt(len - 1) <= ' ')) {
                len--;
            }
            comment = comment.substring(3, len);
            proto.addDoc(comment);
        }
    ;

header_syntax [Proto proto]
    :   SYNTAX ASSIGN STRING_LITERAL SEMICOLON! {
            if(!"proto2".equals(getStringFromStringLiteral($STRING_LITERAL.text))) {
                throw new IllegalStateException("Syntax isn't proto2: '" +
                  getStringFromStringLiteral($STRING_LITERAL.text)+"'");
            }
                  
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        }
    ;

header_package [Proto proto]
@init {
    String value = null;
}
    :   PKG (FULL_ID { value = $FULL_ID.text; } | var { value = $var.text; }) SEMICOLON! {
            if(proto.getPackageName() != null)
                throw new IllegalStateException("Multiple package definitions.");
            
            proto.setPackageName(value);
            
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        }
    ;
    
header_import [Proto proto]
    :   IMPORT STRING_LITERAL SEMICOLON! {
            proto.importProto(getStringFromStringLiteral($STRING_LITERAL.text));
            
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        }
    ;

option_entry [Proto proto, HasOptions ho]
    :   OPTION LEFTPAREN? k=var_full RIGHTPAREN? ASSIGN (
                vr=var_reserved { ho.putExtraOption($k.text, $vr.text); }
            |   id=ID { ho.putStandardOption($k.text, $id.text); }
            |   fid=FULL_ID { ho.putStandardOption($k.text, $fid.text); }
            |   NUMFLOAT { ho.putExtraOption($k.text, Float.valueOf($NUMFLOAT.text)); }
            |   NUMINT { ho.putExtraOption($k.text, Integer.valueOf($NUMINT.text)); }
            |   NUMDOUBLE { ho.putExtraOption($k.text, Double.valueOf($NUMDOUBLE.text)); }
            |   TRUE { ho.putExtraOption($k.text, Boolean.TRUE); }
            |   FALSE { ho.putExtraOption($k.text, Boolean.FALSE); }
            |   STRING_LITERAL { ho.putExtraOption($k.text, getStringFromStringLiteral($STRING_LITERAL.text)); }
        ) SEMICOLON! {
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        }
    ;
    
message_block [Proto proto, Message parent]
@init {
    Message message = null;
}
    :   MESSAGE ID { 
            message = new Message($ID.text, parent, proto);
            proto.addAnnotationsTo(message);
        } 
        LEFTCURLY (message_body[proto, message])* RIGHTCURLY {
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        }
    ;

message_body [Proto proto, Message message]
    :   message_block[proto, message]
    |   message_field[proto, message]
    |   enum_block[proto, message]
    |   service_block[proto, message]
    |   extend_block[proto, message]
    |   extensions_range[proto, message]
    |   annotation_entry[proto]
    |   doc_entry[proto]
    |   option_entry[proto, message]
    ;
    
extensions_range [Proto proto, Message message]
@init {
  int first = -1;
  int last = -1;
}
    :   EXTENSIONS f=NUMINT { first = Integer.parseInt($f.text); last = first;}
        ( TO ( l=NUMINT { last = Integer.parseInt($l.text); } | MAX {last = 536870911; } ) )?
        SEMICOLON! {
            message.defineExtensionRange(first, last);
        }
    ;
    
message_field [Proto proto, HasFields message]
@init {
    Field.Modifier modifier = null;
    FieldHolder fieldHolder = null;
}
    :   (OPTIONAL { modifier = Field.Modifier.OPTIONAL;  } 
        |   REQUIRED { modifier = Field.Modifier.REQUIRED; } 
        |   REPEATED { modifier = Field.Modifier.REPEATED; }) {
            fieldHolder = new FieldHolder();
        }
        field_type[proto, message, fieldHolder] 
        var ASSIGN NUMINT {
            if(fieldHolder.field != null) {
                fieldHolder.field.modifier = modifier;
                fieldHolder.field.name = $var.text;
                fieldHolder.field.number = Integer.parseInt($NUMINT.text);
            }
        } 
        (field_options[proto, message, fieldHolder.field])? {
            if(fieldHolder.field != null) {
                proto.addAnnotationsTo(fieldHolder.field, message.getEnclosingNamespace());
                message.addField(fieldHolder.field);
            }
        }
        (SEMICOLON! | ignore_block)
    ;
    
field_type [Proto proto, HasFields message, FieldHolder fieldHolder]
    :   INT32 { fieldHolder.setField(new Field.Int32()); }
    |   UINT32 { fieldHolder.setField(new Field.UInt32()); }
    |   SINT32 { fieldHolder.setField(new Field.SInt32()); }
    |   FIXED32 { fieldHolder.setField(new Field.Fixed32()); }
    |   SFIXED32 { fieldHolder.setField(new Field.SFixed32()); }
    |   INT64 { fieldHolder.setField(new Field.Int64()); }
    |   UINT64 { fieldHolder.setField(new Field.UInt64()); }
    |   SINT64 { fieldHolder.setField(new Field.SInt64()); }
    |   FIXED64 { fieldHolder.setField(new Field.Fixed64()); }
    |   SFIXED64 { fieldHolder.setField(new Field.SFixed64()); }
    |   FLOAT { fieldHolder.setField(new Field.Float()); }
    |   DOUBLE { fieldHolder.setField(new Field.Double()); }
    |   BOOL { fieldHolder.setField(new Field.Bool()); }
    |   STRING { fieldHolder.setField(new Field.String()); }
    |   BYTES { fieldHolder.setField(new Field.Bytes()); }
    |   GROUP {
            String suffix = proto.getFile()==null ? "" : " of " + proto.getFile().getName();
            warn("'group' not supported @ line " + $GROUP.line + suffix);
        }
    |   FULL_ID {
            String fullType = $FULL_ID.text;
            int lastDot = fullType.lastIndexOf('.');
            String packageName = fullType.substring(0, lastDot); 
            String type = fullType.substring(lastDot+1);
            fieldHolder.setField(new Field.Reference(packageName, type, message));
        }
    |   ID { 
            String type = $ID.text;
            fieldHolder.setField(new Field.Reference(null, type, message));
        }
    ;
    
field_options [Proto proto, HasFields message, Field field]
    :   LEFTSQUARE field_options_keyval[proto, message, field, true] 
        (COMMA field_options_keyval[proto, message, field, true])* RIGHTSQUARE
    ;
    
field_options_keyval [Proto proto, HasFields message, Field field, boolean checkDefault]
    :   key=var_full ASSIGN (vr=var_reserved {
            field.putExtraOption($key.text, $vr.text);
        } 
    |   STRING_LITERAL {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.String)
                    field.defaultValue = getStringFromStringLiteral($STRING_LITERAL.text);
                else if(field instanceof Field.Bytes)
                    field.defaultValue = getBytesFromStringLiteral($STRING_LITERAL.text);
                else
                    throw new IllegalStateException("Invalid string default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, getStringFromStringLiteral($STRING_LITERAL.text));
        }
    |   NUMFLOAT {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.Float)
                    field.defaultValue = Float.valueOf($NUMFLOAT.text);
                else if(field instanceof Field.Double) 
                    field.defaultValue = Double.valueOf($NUMFLOAT.text);
                else
                    throw new IllegalStateException("Invalid float default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, Float.valueOf($NUMFLOAT.text));
        } 
    |   NUMINT {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.Number) {
                    if(field.getClass().getSimpleName().endsWith("32"))
                        field.defaultValue = Integer.valueOf($NUMINT.text);
                    else if(field.getClass().getSimpleName().endsWith("64"))
                        field.defaultValue = Long.valueOf($NUMINT.text);
                    else if(field instanceof Field.Float)
                        field.defaultValue = Float.valueOf($NUMINT.text);
                    else if(field instanceof Field.Double) 
                        field.defaultValue = Double.valueOf($NUMINT.text);
                    else
                        throw new IllegalStateException("Invalid numeric default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
                }
                else
                    throw new IllegalStateException("Invalid numeric default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, Integer.valueOf($NUMINT.text));
        }
    |   NUMDOUBLE {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");

                if(field instanceof Field.Float)
                    field.defaultValue = Float.valueOf($NUMDOUBLE.text);
                else if(field instanceof Field.Double) 
                    field.defaultValue = Double.valueOf($NUMDOUBLE.text);
                else
                    throw new IllegalStateException("Invalid numeric default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, Double.valueOf($NUMDOUBLE.text));
        }
    |   HEX {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.Number) {
                    if(field instanceof Field.Int32)
                        field.defaultValue = new Integer(TextFormat.parseInt32($HEX.text));
                    else if(field instanceof Field.UInt32)
                        field.defaultValue = new Integer(TextFormat.parseUInt32($HEX.text));
                    else if(field instanceof Field.Int64)
                        field.defaultValue = new Long(TextFormat.parseInt64($HEX.text));
                    else if(field instanceof Field.UInt64)
                        field.defaultValue = new Long(TextFormat.parseUInt64($HEX.text));
                    else if(field instanceof Field.Float)
                        field.defaultValue = new Float(Long.decode($HEX.text).floatValue());
                    else if(field instanceof Field.Double) 
                        field.defaultValue = new Double(Long.decode($HEX.text).doubleValue());
                }
                else if(field instanceof Field.Bytes) {
                    field.defaultValue = getBytesFromHexString($HEX.text);
                }
                else
                    throw new IllegalStateException("Invalid numeric default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
                
            }
            
            field.putExtraOption($key.text, $HEX.text);
        }
    |   OCTAL {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.Number) {
                    if(field instanceof Field.Int32)
                        field.defaultValue = new Integer(TextFormat.parseInt32($OCTAL.text));
                    else if(field instanceof Field.UInt32)
                        field.defaultValue = new Integer(TextFormat.parseUInt32($OCTAL.text));
                    else if(field instanceof Field.Int64)
                        field.defaultValue = new Long(TextFormat.parseInt64($OCTAL.text));
                    else if(field instanceof Field.UInt64)
                        field.defaultValue = new Long(TextFormat.parseUInt64($OCTAL.text));
                    else if(field instanceof Field.Float)
                        field.defaultValue = new Float(Long.decode($OCTAL.text).floatValue());
                    else if(field instanceof Field.Double) 
                        field.defaultValue = new Double(Long.decode($OCTAL.text).doubleValue());
                }
                else
                    throw new IllegalStateException("Invalid numeric default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, $OCTAL.text);
        }
    |   TRUE {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.Bool)
                    field.defaultValue = Boolean.TRUE;
                else
                    throw new IllegalStateException("invalid boolean default value for the non-boolean field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, Boolean.TRUE);
        }    
    |   FALSE {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.Bool)
                    field.defaultValue = Boolean.FALSE;
                else
                    throw new IllegalStateException("invalid boolean default value for the non-boolean field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, Boolean.FALSE);
        }
    |   val=ID {
            boolean refOption = false;
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                String refName = $val.text;
                if(field instanceof Field.Reference)
                    field.defaultValue = refName;
                else if(field instanceof Field.Float) {
                    if("inf".equals(refName)) {
                        field.defaultValue = Float.POSITIVE_INFINITY;
                        field.defaultValueConstant = "Float.POSITIVE_INFINITY";
                    }
                    else if("nan".equals(refName)) {
                        field.defaultValue = Float.NaN;
                        field.defaultValueConstant = "Float.NaN";
                    }
                    else
                        throw new IllegalStateException("Invalid float default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
                }
                else if(field instanceof Field.Double) {
                    if("inf".equals(refName)) {
                        field.defaultValue = Double.POSITIVE_INFINITY;
                        field.defaultValueConstant = "Double.POSITIVE_INFINITY";
                    }
                    else if("nan".equals(refName)) {
                        field.defaultValue = Double.NaN;
                        field.defaultValueConstant = "Double.NaN";
                    }
                    else
                        throw new IllegalStateException("Invalid double default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
                }   
                else {
                    refOption = true;
                    //throw new IllegalStateException("invalid field value '" + refName + "' for the field: " + field.getClass().getSimpleName() + " " + field.name);
                }
            }
            else {
                refOption = true;
            }
            
            if(refOption)
                field.putStandardOption($key.text, $val.text);
            else
                field.putExtraOption($key.text, $val.text);
        }
    |   FULL_ID {
            field.putStandardOption($key.text, $FULL_ID.text);
        }
    |   EXP {
            if(checkDefault && "default".equals($key.text)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                if(field instanceof Field.Float)
                    field.defaultValue = Float.valueOf($EXP.text);
                else if(field instanceof Field.Double) 
                    field.defaultValue = Double.valueOf($EXP.text);
                else
                    throw new IllegalStateException("Invalid float default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
            }
            
            field.putExtraOption($key.text, $EXP.text);
        }
    |   signed_constant[proto, message, field, $key.text, checkDefault] {
            field.putExtraOption($key.text, $signed_constant.text);
        }
        )
    ;
    
signed_constant [Proto proto, HasFields message, Field field, String key, boolean checkDefault]
    :   MINUS ID {
            if(checkDefault && "default".equals(key)) {
                if(field.defaultValue!=null || field.modifier == Field.Modifier.REPEATED)
                    throw new IllegalStateException("a field can only have a single default value");
                
                String refName = $ID.text;
                if(field instanceof Field.Float) {
                    if("inf".equals(refName)) {
                        field.defaultValue = Float.NEGATIVE_INFINITY;
                        field.defaultValueConstant = "Float.NEGATIVE_INFINITY";
                    }
                    else
                        throw new IllegalStateException("Invalid float default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
                }
                else if(field instanceof Field.Double) {
                    if("inf".equals(refName)) {
                        field.defaultValue = Double.NEGATIVE_INFINITY;
                        field.defaultValueConstant = "Double.NEGATIVE_INFINITY";
                    }
                    else
                        throw new IllegalStateException("Invalid double default value for the field: " + field.getClass().getSimpleName() + " " + field.name);
                }   
                else
                    throw new IllegalStateException("invalid field value '" + refName + "' for the field: " + field.getClass().getSimpleName() + " " + field.name);
            }
        }
    ;
    
enum_block [Proto proto, Message message]
@init {
    EnumGroup enumGroup = null;
}
    :   ENUM ID { 
            enumGroup = new EnumGroup($ID.text, message, proto);
            proto.addAnnotationsTo(enumGroup);
        } 
        LEFTCURLY (enum_body[proto, message, enumGroup])* RIGHTCURLY {
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        } (SEMICOLON?)!
    ;
    
enum_body [Proto proto, Message message, EnumGroup enumGroup]
    :   enum_field[proto, message, enumGroup]
    |   annotation_entry[proto]
    |   doc_entry[proto]
    |   option_entry[proto, enumGroup]
    ;

enum_field [Proto proto, Message message, EnumGroup enumGroup]
@init {
    EnumGroup.Value v = null;
}
    :   ID ASSIGN NUMINT {
            v = new EnumGroup.Value($ID.text, Integer.parseInt($NUMINT.text), enumGroup);
            proto.addAnnotationsTo(v);
        } (enum_options[proto, enumGroup, v])? SEMICOLON! 
    ;

enum_options [Proto proto, EnumGroup enumGroup, EnumGroup.Value v]
    :   LEFTSQUARE field_options_keyval[proto, null, v.field, false] 
        (COMMA field_options_keyval[proto, null, v.field, false])* RIGHTSQUARE
    ;
    
service_block [Proto proto, Message message]
@init {
    Service service = null;
}
    :   SERVICE ID { 
            service = new Service($ID.text, message, proto); 
            proto.addAnnotationsTo(service);
        } LEFTCURLY
        (service_body[proto, service])+ RIGHTCURLY (SEMICOLON?)! {
            if(service.rpcMethods.isEmpty())
                throw new IllegalStateException("Empty Service block: " + service.getName());
                
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        }
    ;
    
service_body [Proto proto, Service service]
    :   rpc_block[proto, service]
    |   annotation_entry[proto]
    |   doc_entry[proto]
    |   option_entry[proto, service]
    ;
    
rpc_block [Proto proto, Service service]
@init {
    String argName = null, argPackage = null, retName = null, retPackage = null;
    Service.RpcMethod rm = null;
}
    :   RPC n=ID LEFTPAREN (ap=FULL_ID {  
            String argFull = $ap.text;
            int lastDot = argFull.lastIndexOf('.');
            argPackage = argFull.substring(0, lastDot); 
            argName = argFull.substring(lastDot+1);
        } | a=(VOID|ID) { argName = $a.text; }) RIGHTPAREN 
        RETURNS LEFTPAREN (rp=FULL_ID {  
            String retFull = $rp.text;
            int lastDot = retFull.lastIndexOf('.');
            retPackage = retFull.substring(0, lastDot); 
            retName = retFull.substring(lastDot+1);
        } | r=(VOID|ID) { retName = $r.text; }) RIGHTPAREN {
            rm = service.addRpcMethod($n.text, argName, argPackage, retName, retPackage);
            proto.addAnnotationsTo(rm);
        } rpc_body_block[proto, rm]? SEMICOLON!
    ;
    
rpc_body_block [Proto proto, Service.RpcMethod rm]
    :   LEFTCURLY option_entry[proto, rm]* RIGHTCURLY {
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
        }
    ;
    
extend_block [Proto proto, Message parent]
@init {
    Extension extension = null;
}
    :   EXTEND (
        FULL_ID {
            String fullType = $FULL_ID.text;
            int lastDot = fullType.lastIndexOf('.');
            String packageName = fullType.substring(0, lastDot); 
            String type = fullType.substring(lastDot+1);
            extension = new Extension(proto, parent, packageName, type);
        } | ID { extension = new Extension(proto, parent, null, $ID.text); } ) {
            if(parent==null)
                proto.addExtension(extension);
            else
                parent.addNestedExtension(extension);
                
            proto.addAnnotationsTo(extension);
        }
        LEFTCURLY (extend_body[proto, extension])* RIGHTCURLY {
            if(!proto.annotations.isEmpty())
                throw new IllegalStateException("Misplaced annotations: " + proto.annotations);
            if(!proto.docs.isEmpty())
                throw new IllegalStateException("Misplaced docs: " + proto.docs);
                
        } (SEMICOLON?)!
    ;
    
extend_body [Proto proto, Extension extension]
    :   message_field[proto, extension]
    |   annotation_entry[proto]
    |   doc_entry[proto]
    ;
    
ignore_block
    :   LEFTCURLY ignore_block_body* RIGHTCURLY
    ;
    
ignore_block_body
    :   (LEFTCURLY)=> ignore_block
    |   ~RIGHTCURLY
    ;
    
